Spring Security

Spring Security

一. 简介

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

什么是ACL和RBAC

  • ACL: Access Control List 访问控制列表
    • 以前盛行的一种权限设计,它的核心在于用户直接和权限挂钩
    • 优点:简单易用,开发便捷
    • 缺点:用户和权限直接挂钩,导致在授予时的复杂性,比较分散,不便于管理
    • 例子:常见的文件系统权限设计, 直接给用户加权限
  • RBAC: Role Based Access Control
    • 基于角色的访问控制系统。权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限
    • 优点:简化了用户与权限的管理,通过对用户进行分类,使得角色与权限关联起来
    • 缺点:开发对比ACL相对复杂
    • 例子:基于RBAC模型的权限验证框架与应用 Apache Shiro、spring Security
  • BAT企业 ACL,一般是对报表系统,阿里的ODPS

二. 入门案例

2.1 添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
</dependencies>

2.2 请求

​ 我们任意编写一个接口,然后进行访问,会直接跳转到一个登录页面

三. 自定义用户登录处理

3.1 安全配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

http.formLogin() //采用表单登录
.and()
.authorizeRequests() //请求认证
.anyRequest() //对于任何请求都需要认证
.authenticated(); //认证通过了才能访问
}
}

3.2 自定义用户认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class UserAuthentication implements UserDetailsService {

@Resource
private SysUserRepository sysUserRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

SysUser sysUser = sysUserRepository.findByNickyName(username);

if (null == sysUser) {
return new User(username, null, null);
}else {
return new User(username, sysUser.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
}

​ 在实际的应用过程中,当我们发起请求的时候,springSecurity处理用户登录的过滤器是UsernamePasswordAuthenticationFilter这个过滤器,而这个过滤器会将用户提交的用户名和密码交由UserDetailsService的实现类来处理。具体的处理流程如下图所示:

3.3 密码加密校验

​ 密码的加密校验需要实现PasswordEncoder这个接口,接口中有两个方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class CustomizePasswordEncoder implements PasswordEncoder {

// 注册的时候使用, 人为的去调用
@Override
public String encode(CharSequence rawPassword) {
return null;
}

// 当在返回UserDetails,会自动的去实现校验
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return false;
}
}

​ 实际工作中我们可以直接使用spring security中默认的密码处理方式就完全可以满足日常的开发。

3.4 自定义登录页面

​ spring security中定义的登录页面有可能不满足需求,需要自己来实现一个登录页面,处理的方式为只需要在3.1节方法中 formLogin() 方法的后面加上loginPage()方法即可,如下代码所示:

1
2
3
4
5
6
7
8
9
10
@Override
protected void configure(HttpSecurity http) throws Exception {

http.formLogin() //采用表单登录
.loginPage("/login.html")
.and()
.authorizeRequests() //请求认证
.anyRequest() //对于任何请求都需要认真
.authenticated(); //认证通过了才能访问
}

这样配置会发现报如下的错误:

​ 这个错误是很多的初学者容易犯的一个错误,原因是因为对于任何的页面都需要认证,所以就在这里无限循环下去了。我们需要接着调整代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void configure(HttpSecurity http) throws Exception {

http.formLogin() //采用表单登录
.loginPage("/login.html")
.loginProcessingUrl("/authentication/form") //登录页面提交的地址
.and()
.authorizeRequests() //请求认证
.antMatchers("/login.html").permitAll() //如果是登录页面直接让其访问
.anyRequest() //对于任何请求都需要认真
.authenticated(); //认证通过了才能访问
}

3.5 编写自己的登录页面

1
2
3
4
5
<form action="/authentication/form" method="post">
Username: <input name="username"> <br>
Password: <input name="password" type="password"> <br>
<button>提交</button>
</form>

当我们实现了自己的登录页面后发现还是无法登录,原因在于我们没有加上csrf(跨站请求伪造),我们暂时先将其禁用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void configure(HttpSecurity http) throws Exception {

http.formLogin() //采用表单登录
.loginPage("/login.html")
.loginProcessingUrl("/authentication/form") //登录页面提交的地址
.and()
.authorizeRequests() //请求认证
.antMatchers("/login.html").permitAll() //如果是登录页面直接让其访问
.anyRequest() //对于任何请求都需要认真
.authenticated() //认证通过了才能访问
.and()
.csrf().disable(); //关闭跨站请求伪造功能
}

四. 登录成功与失败处理

4.1 登录成功处理

​ 在spring security中,当我们登录成功后默认是跳转到用户登录之前的请求,这个在当今SPA(Single Page Application)应用流行的今天,肯定是不适用的,我们需要的是异步的请求,返回登录成功的信息。

​ 要实现用户登录成功处理,需要实现AuthenticationSuccessHandler这个接口,然后实现接口中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
public class CustomizeAuthenticationSuccessHanler
implements AuthenticationSuccessHandler {

private Logger logger = LoggerFactory
.getLogger(CustomizeAuthenticationSuccessHanler.class);

//该bean是springmvc启动的时候实例化的一个对象,纳入到容器中
@Autowired
private ObjectMapper objectMapper;

/**
* authentication中包含了用户的各种信息,包括UserDetail信息
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {

logger.info("登录成功");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}

4.2 登录失败处理

​ 通过上面的演示我们能看到每次登录,还是回到登录页面,在异步请求下这种是无法满足我们的需求的,所以需要自定义登录失败处理。要实现AuthenticationFailureHandler这个接口,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
public class CustomerAuthenticationFailHandler
implements AuthenticationFailureHandler {

private Logger logger = LoggerFactory
.getLogger(CustomerAuthenticationFailHandler.class);

@Autowired
private ObjectMapper objectMapper;

@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
logger.info("登录失败");

Map<String, Object> map = new HashMap<>();
map.put("code", -1);
map.put("msg", "用户名或密码错误");

response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(map));
}
}

安全配置代码如下:

五. 记住我

5.1 基本原理

​ 在前端页面的请求参数必须叫remember-me

5.2 功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private UserDetailsService userAuthentication;

@Autowired
private DataSource dataSource;

// 该Bean的主要作用是,可以用于创建数据表并且,当用户直接访问的时候直接从
// 数据库查询用户信息
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository
= new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}

5.3 安全配置

六. 图片验证码

​ 在实际的应用过程中,为了防止用户的恶意请求,我们通常都会设置图片验证码功能,而springsecurity并没有提供现有的实现,需要开发人员自行的实现。

6.1 封装验证码类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ImageCode {
private BufferedImage bufferedImage;
// code是随机字母,需要存储在session中
private String code;
// 过期时间
private LocalDateTime expireTime;

// 第三个参数为过期的时间
public ImageCode(BufferedImage bufferedImage, String code, int seconds) {
this.bufferedImage = bufferedImage;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(seconds); //设置过期的时间点
}

// 验证码是否过期
public boolean isExpire() {
return LocalDateTime.now().isAfter(expireTime); //当前时间是否在过期时间之后
}

// setters、getters、other constructors
}

6.2 请求控制类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class ImageCodeController {

//操作Session的工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

//session中存放验证码的key
public static final String IMAGE_CODE_SESSION_KEY = "IMAGE_CODE_SESSION_KEY";

@RequestMapping("/image/code")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {

//生成图片验证码,具体的实现照搬现有的工具类
ImageCode imageCode = generate();

//将图片验证码存放到session中,
sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CODE_SESSION_KEY, imageCode);

//将图片写回到页面
ImageIO.write(imageCode.getBufferedImage(), "JPEG", response.getOutputStream());
}
}

6.3 过滤器的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class ValidataCodeFilter extends OncePerRequestFilter {
// 所有登录失败都交由该类来处理
private CustomerAuthenticationFailHandler customerAuthenticationFailHandler;

public void setCustomerAuthenticationFailHandler(CustomerAuthenticationFailHandler customerAuthenticationFailHandler) {
this.customerAuthenticationFailHandler = customerAuthenticationFailHandler;
}

private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//判断是否为登录,并且请求方式为post
if(StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equals(request.getMethod(), "POST")) {
try{
validate(new ServletWebRequest(request)); //校验验证码
}catch (AuthenticationException exception) {
customerAuthenticationFailHandler.onAuthenticationFailure(request, response, exception);
return;
}
}
filterChain.doFilter(request, response);
}

// 具体的校验逻辑
public void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 从session中获取验证码信息
ImageCode imageCode = (ImageCode) sessionStrategy.getAttribute(request,
ImageCodeController.IMAGE_CODE_SESSION_KEY);

// 获取请求的参数
String validateCode = ServletRequestUtils.getStringParameter(request.getRequest(), "codeImage");

if(StringUtils.isEmpty(validateCode)) {
throw new ValidateException("验证码不能为空");
}

if(imageCode == null) {
throw new ValidateException("验证码不存在");
}

if(imageCode.isExpire()) {
throw new ValidateException("验证码过期");
sessionStrategy.removeAttribute(request,
ImageCodeController.IMAGE_CODE_SESSION_KEY);
}

if(!validateCode.equals(imageCode.getCode())) {
throw new ValidateException("验证码不正确");
}

sessionStrategy.removeAttribute(request,
ImageCodeController.IMAGE_CODE_SESSION_KEY);
}
}

6.4 登录异常处理

​ springSecurity中处理用户登录异常都应该由AuthenticationException这个异常来处理,所以我们需要自定义验证码校验失败的异常类:

1
2
3
4
5
6
public class ValidateException extends AuthenticationException {

public ValidateException(String msg) {
super(msg);
}
}

七. 手机号登录

​ 手机号登录与用户名密码登录逻辑相同,所以我们在使用手机号登录系统的时候可以完全拷贝用户名密码登录的逻辑,那么前提是我们必须得搞懂用户名密码登录的逻辑。

7.1 编写Token

​ 编写手机号认证Token, 模仿UsernamePasswordAuthenticationToken这个类来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 短信验证码Token, 用于封装用户使用手机登录的相关信息。
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

// principal在未登录之前封装用户的手机号,登录之后封装用户的信息
private final Object principal;

// ~ Constructors
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.

*/
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}

/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param authorities
*/
public SmsAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}

@Override
public Object getCredentials() {
return null;
}

public Object getPrincipal() {
return this.principal;
}

public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}

super.setAuthenticated(false);
}

@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}

7.2 编写Filter

​ 手机号的过滤器可以模仿 UsernamePasswordAuthenticationFilter 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class SmsAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;

public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}

// ~ Methods
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String mobile = obtainMobile(request);

if (mobile == null) {
mobile = "";
}

mobile = mobile.trim();

SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}


protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}

/**
* Provided so that subclasses may configure what is put into the authentication
* request's details property.
*
* @param request that an authentication request is being created for
* @param authRequest the authenticatio request object that should have its details
*/
protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
*/
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}

7.3 Provider

​ Provider的作用是用来处理对应的Token,校验用户名密码使用的Provider为DaoAuthenticationProvider, 在实现我们自己的Provider的时候,我们去实现AuthenticationProvider。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class SmsCredentialsProvider implements AuthenticationProvider {

private UserDetailsService userDetailsService;

public UserDetailsService getUserDetailsService() {
return userDetailsService;
}

public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

//实际传入过来的就是 SmsAuthenticationToken, 因为supports方法已经进行了判断,如果为true,才进入该方法
SmsAuthenticationToken smsAuthenticationToken = (SmsAuthenticationToken)authentication;

//使用
UserDetails user = userDetailsService.loadUserByUsername((String)smsAuthenticationToken.getPrincipal());

if(null == user) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}

//将用户信息以及用户权限 重新构建一个SmsAuthenticationToken
SmsAuthenticationToken token = new SmsAuthenticationToken(user, user.getAuthorities());
token.setDetails(smsAuthenticationToken.getDetails());

return token;
}

/**
* 判断当前的方法的参数authentication, 是否为SmsAuthenticationToken这个类型,
* 如果是的化,就调用上面的 authenticate 方法。
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}

7.4 发送短信实现

接口的实现

1
2
3
4
public interface SmsCodeSender {
//发送手机短信验证码的
void send(String code, String mobile);
}

实现类

1
2
3
4
5
6
7
public class DefaultSmsCodeSender implements SmsCodeSender {

@Override
public void send(String code, String mobile) {
System.out.println("往手机 " + mobile + " 上发送的验证码为: " + code);
}
}

7.5 配置Filter以及Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
public class SmsCodeAuthenticationConfig
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired
private CustomizeAuthenticationSuccessHanler customizeAuthenticationSuccessHanler;

@Autowired
private CustomerAuthenticationFailHandler customerAuthenticationFailHandler;

@Autowired
private UserDetailsService userAuthentication;

@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();

//设置AuthenticationManager, 用于同一管理Filter
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//设置成功处理器
smsAuthenticationFilter.setAuthenticationSuccessHandler(customizeAuthenticationSuccessHanler);
//设置失败过滤器
smsAuthenticationFilter.setAuthenticationFailureHandler(customerAuthenticationFailHandler);

//实例化Provider
SmsCredentialsProvider smsCredentialsProvider = new SmsCredentialsProvider();
smsCredentialsProvider.setUserDetailsService(userAuthentication);

http.authenticationProvider(smsCredentialsProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}

7.6 安全配置

​ 短信验证码的过滤器和图片验证码的逻辑是相同,故在此不作处理。

7.7 页面的实现

八. session管理

8.1 session并发控制

​ session的失效时间默认为30min,可以通过 server.servlet.session.timeout类配置。在很多的业务场景下,我们只允许一台设备登录到服务端。

安全配置

session失效处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 同时多设备登录处理
*/
public class MultipleSessionHandler implements SessionInformationExpiredStrategy {

@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write("其他设备登录");
}
}

8.2 session集群管理

​ 当我们在集群环境下,用户每次的请求我们并不能保证每次都是到达同一台服务器,可能会导致session存在于不同的服务器上,而让用户重新进行登录,所以必须要采用一个中间件来存储用户的session信息,企业中使用最多的就是redis.

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.7.0</version>
</dependency>

applicatoin.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
spring:
redis:
port: 6379
host: localhost
password:

lettuce:
pool:
min-idle: 2
max-active: 8
session:
store-type: redis

九. 退出登录

1
2
3
.logout()  //
.logoutSuccessUrl("/login.html") //退出后跳转的页面
.and()

十. 权限管理

​ 权限是大部分的后台管理系统都需要实现的功能,用户控制不同的角色能够进行的不同的操作。Spring Security的可以进行用户的角色权限控制,也可以进行用户的操作权限控制。在之前的代码实现上,我们仅仅只是实现用户的登录,在用户信息验证的时候使用UserDetailsService,但是却一直忽略了用户的权限。

10.1 启动类配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 开启方法的注解安全校验。
* securedEnabled @Secured("ROLE_abc") 该注解是Spring security提供的
* jsr250Enabled @RolesAllowed("admin") 该注解是 JSR250 支持的注解形式
* prePostEnabled
*/
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityApplication {

public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}

10.2 基于角色的权限控制

用户权限的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class UserSecurityService implements UserDetailsService {


@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

/**
* 调用形式有两种:
* 1. 此时构建的 SimpleGrantedAuthority 必须是以 ROLE_ 开头, 例如 ROLE_admin, ROLE_manager.
* 实现全权限控制的时候使用 @RolesAllowed("ROLE_admin") 或者 @RolesAllowed("admin") 都可以
* 2. 此时构建的 SimpleGrantedAuthority 必须是以 ROLE_ 开头, 例如 ROLE_admin, ROLE_manager.
* 实现全权限控制的时候使用 @Secured("ROLE_admin") ROLE_是不能省略的。
*
* 其中1,2也只能实现对角色的控制,那么如果细粒度到具体的方法进行控制,需要使用到其他的方式。
* A. new SimpleGrantedAuthority("user:delete") @PreAuthorize("hasAnyAuthority('user:add', 'user:list')") 无法访问。
* B. new SimpleGrantedAuthority("user:add") @PreAuthorize("hasAnyAuthority('user:add', 'user:list')") 可以访问。
* C. Arrays.asList(new SimpleGrantedAuthority("user:add"), new SimpleGrantedAuthority("user:list"))
* @PreAuthorize("hasAuthority('user:add') and hasAuthority('user:list')") 可以访问
* D. new SimpleGrantedAuthority("ROLE_admin") 定义角色
* @PreAuthorize("hasRole('admin')") 可以访问
*
*
*/
return new User(username, sysUser.getPassword(),
Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
}
}

我们在构建SimpleGrantedAuthority对象的时候,用户的角色必须是以 ROLE_ 开头,例如 ROLE_adminROLE_manager

控制器角色控制

​ 在控制器上进行用户访问控制的时候,基于角色有两种书写方式:

方式一:@RolesAllowed

1
2
3
4
5
6
7
8
9
/**
* @RolesAllowed 中的值可以写成 "admin", 例如 @RolesAllowed("admin")
* @RolesAllowed 中的值还可以写成 "ROLE_admin",例如 @RolesAllowed("ROLE_admin")
*/
@RequestMapping
@RolesAllowed("admin")
public Object getAll() {
return Arrays.asList(new User(10, "张"), new User(20, "李四"));
}

方式二:

1
2
3
4
5
6
7
8
/**
* @Secured 中的值必须为 "ROLE_admin",例如 @Secured("ROLE_admin"),ROLE_不能省略
*/
@RequestMapping
@Secured("ROLE_admin")
public Object getAll() {
return Arrays.asList(new User(10, "张"), new User(20, "李四"));
}

10.3 基于操作的权限控制

​ 当然我们也可以使用基于操作的权限控制,这个功能稍显得有点累赘,因为在实际的项目开发过程中我们都是基于角色的权限控制。

用户权限查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class UserSecurityService implements UserDetailsService {


@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
return new User(username, sysUser.getPassword(),
Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
*/
return new User(username, sysUser.getPassword(),
Arrays.asList(new SimpleGrantedAuthority("user:list"),
new SimpleGrantedAuthority("user:add")
));
}
}

控制器访问控制(针对角色)

1
2
3
4
5
6
7
8
9
10
/**
* @PreAuthorize 中的值可以为 "ROLE_admin", "admin",
* 例如 @PreAuthorize("hasRole('admin')") 或者为
* @PreAuthorize("hasRole('ROLE_admin')")
*/
@RequestMapping
@PreAuthorize("hasRole('admin')")
public Object getAll() {
return Arrays.asList(new User(10, "张"), new User(20, "李四"));
}

控制器访问控制(针对操作)

1
2
3
4
5
6
7
@RequestMapping
// @PreAuthorize("hasAuthority('user:add') and hasAuthority('user:list')")
// @PreAuthorize("hasAuthority('user:add') or hasAuthority('user:list')")
@PreAuthorize("hasAnyAuthority('user:add', 'user:list')")
public Object getAll() {
return Arrays.asList(new User(10, "张"), new User(20, "李四"));
}

10.4 访问无权限处理

1
2
3
4
.and()
.exceptionHandling()
.accessDeniedHandler(customizeAccessDeniedHandler) //无权限访问处理
.and()
© 2019 Sprider All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero